import logging
import time

from .base import DBManager, JsonMixin

logger = logging.getLogger('server.manager.redis')


class RedisManager(DBManager):
    BASE_LOGGER = logger
    expire_in = 60 * 60 * 24 * 3
    default_expire = 60 * 60 * 24 * 30

    async def _get_connection(self):
        return await self.service.connection.get_connection(self.db)

    async def execute(self, command, *args):
        connection = await self._get_connection()
        self.logger.debug('[connection: %s] [command: %s] [args: %s]',
                          connection, command, args)
        return await connection.execute(command, *args)

    async def default_refresh(self, key):
        return await self.execute('EXPIRE', key, self.default_expire)


class ListMixin:
    async def append(self, value):
        value = self.serialize(value)
        length_after = await self.execute('RPUSH', self.list_key, value)
        await self.default_refresh(self.list_key)
        return length_after

    async def update(self, index, value):
        value = self.serialize(value)
        is_updated = await self.execute('LSET', self.list_key, index, value)
        await self.default_refresh(self.list_key)
        return is_updated

    async def count(self):
        return await self.execute('LLEN', self.list_key)

    async def slicing(self, start, end):
        end -= 1
        lst = await self.execute('LRANGE', self.list_key, start, end)
        return [self.deserialize(item) for item in lst]

    async def all(self):
        return await self.slicing(0, 1000)

    async def clear(self):
        await self.execute('DEL', self.list_key)


class HashMixin:
    async def get(self, key):
        value = await self.execute('HGET', self.hash_key, key)
        await self.default_refresh(self.hash_key)
        if value:
            return self.deserialize(value)
        return None

    async def add(self, key, value=''):
        return await self.update(key, value)

    async def update(self, key, value=''):
        value = self.serialize(value)
        is_added = await self.execute('HSET', self.hash_key, key, value)
        await self.default_refresh(self.hash_key)
        return bool(is_added)

    async def remove(self, key):
        is_removed = await self.execute('HDEL', self.hash_key, key)
        return bool(is_removed)

    async def pop(self, key):
        return await self.remove(key)

    async def exists(self, key):
        is_existed = await self.execute('HEXISTS', self.hash_key, key)
        return bool(is_existed)

    async def all(self, as_dict=True):
        mixed_lst = await self.execute('HGETALL', self.hash_key)
        mapping = {}
        for index in range(len(mixed_lst) // 2):
            index = index * 2
            key, value = mixed_lst[index], mixed_lst[index + 1]
            value = self.deserialize(value)
            if hasattr(key, 'decode'):
                key = key.decode()
            mapping[key] = value
        return mapping

    async def clear(self):
        await self.execute('DEL', self.hash_key)


class HeartBeatMixin:
    async def ping(self):
        await self.execute('SET', self.heartbeat_key, time.time())
        await self.default_refresh(self.heartbeat_key)

    async def set_timeout(self, timeout):
        await self.execute('SETEX', self.heartbeat_key, timeout)
        await self.default_refresh(self.heartbeat_key)

    async def get_last_ping_time(self):
        return await self.execute('GET', self.heartbeat_key)

    async def clear(self):
        await self.execute('DEL', self.heartbeat_key)


class KeyValueMixin(JsonMixin):
    async def set(self, data):
        serialized_data = self.serialize(data)
        data = await self.execute('SETEX', self.key, self.expire_in,
                                  serialized_data)
        await self.default_refresh(self.key)
        return data

    async def get(self, raise_on_nonexist=False):
        raw = await self.execute('GET', self.key)
        await self.default_refresh(self.key)
        if raw is None and raise_on_nonexist:
            raise ValueError('cannot get data')
        if not raw:
            return {}
        return self.deserialize(raw)

    async def update(self, updated, **defaults):
        data = self.get()
        defaults.update(data)
        defaults.update(**updated)
        return await self.set(defaults)

    async def refresh(self):
        return await self.execute('EXPIRE', self.key)


class SetMixin:
    async def add(self, value):
        value = self.serialize(value)
        await self.execute('SADD', self.set_key, value)
        await self.default_refresh(self.set_key)

    async def all(self):
        lst = await self.execute('SMEMBERS', self.set_key)
        return [self.deserialize(value) for value in lst]

    async def clear(self):
        await self.execute('DEL', self.set_key)


class ZsetMixin:

    async def add(self, score, item):
        value = self.serialize(item)
        data = await self.execute('ZADD', self.zset_key, score, value)
        await self.default_refresh(self.zset_key)
        return data

    async def pop(self, item):
        value = self.serialize(item)
        await self.execute('ZREM', self.zset_key, value)

    async def get_score(self, value):
        return await self.execute('ZSCORE', self.zset_key, value)

    async def all(self, total=500, group_by_score=True):
        """
        return: {score: list[value1, value2]}
        """
        mixed_lst = await self.execute('ZRANGE', self.zset_key, 0, total,
                                       'WITHSCORES')
        dct = {}
        for index in range(len(mixed_lst) // 2):  # in case of 0
            index = index * 2
            value, score = mixed_lst[index], mixed_lst[index + 1]
            score = score.decode()
            value = self.deserialize(value)
            dct.setdefault(score, [])
            dct[score].append(value)
        return dct


class LockMixin:
    release_after = 5

    async def locked(self):
        res = await self.execute('SET', self.lock_key, 1, 'EX',
                                 self.release_after, 'NX')
        return not res

    async def acquire(self):
        return not await self.locked()

    async def release(self):
        return await self.execute('DEL', self.lock_key)
